<template>
  <div id="global-uploader" :class="{ 'global-uploader-single': !global }">
    <!-- 上传 -->
    <uploader
      ref="uploaderRef"
      class="uploader-app"
      :options="initOptions"
      :file-status-text="fileStatusText"
      :auto-start="false"
      @file-added="onFileAdded"
      @file-success="onFileSuccess"
      @file-progress="onFileProgress"
      @file-error="onFileError"
    >
      <uploader-unsupport></uploader-unsupport>

      <uploader-btn id="global-uploader-btn" ref="uploadBtnRef"
        >选择文件</uploader-btn
      >

      <uploader-list v-show="panelShow">
        <template #default="{ fileList }">
          <div class="file-panel" :class="{ collapse: collapse }">
            <div class="file-title">
              <div class="title">文件列表</div>
              <div class="operate">
                <a-button
                  :title="collapse ? '展开' : '折叠'"
                  link
                  @click="collapse = !collapse"
                >
                  <Icon
                    :icon="collapse ? 'ri:fullscreen-fill' : 'ri:subtract-fill'"
                    width="18"
                  />
                </a-button>
                <a-button title="关闭" link @click="close">
                  <Icon icon="ri:close-fill" width="20" />
                </a-button>
              </div>
            </div>

            <ul class="file-list">
              <li v-for="file in fileList" :key="file.id" class="file-item">
                <uploader-file
                  ref="files"
                  :class="['file_' + file.id, customStatus]"
                  :file="file"
                  :list="true"
                ></uploader-file>
              </li>
              <div v-if="!fileList.length" class="no-file">
                <Icon icon="ri:file-3-line" width="16" /> 暂无待上传文件
              </div>
            </ul>
          </div>
        </template>
      </uploader-list>
    </uploader>
  </div>
</template>

<script>
  /**
   *  全局上传插件，两种调用方式
   *   1. 作为全局页面的组件，使用event bus
   *   调用方法：Bus.emit('openUploader', {params: {}, options: {}})
   *               params: 发送给服务器的额外参数；
   *               options：上传选项，目前支持 target、testChunks、mergeFn、accept
   *
   *   监听函数：Bus.on('fileAdded', fn); 文件选择后的回调
   *           Bus.on('fileSuccess', fn); 文件上传成功的回调，监听后记得释放
   *
   *   2. 作为普通组件在单个页面中调用，使用props
   */
  import { ref, watch, computed, nextTick, onMounted } from 'vue';
  import { mergeFile } from '@/api/upload/index';
  import { Message } from '@arco-design/web-vue';
  import { getToken } from '@/utils/auth';
  import { generateMD5 } from './utils/md5';
  import Bus from './utils/bus';

  export default {
    name: 'GlobalUploader',
    props: {
      global: {
        type: Boolean,
        default: true,
      },
      // 发送给服务器的额外参数
      params: {
        type: Object,
      },
      options: {
        type: Object,
      },
    },
    emits: ['fileAdded', 'fileSuccess'],

    setup(props, { emit }) {
      const initOptions = {
        target: '/api/upload',
        chunkSize: '2048000',
        fileParameterName: 'file',
        headers: {
          'x-access-token': getToken(),
        },
        maxChunkRetries: 3,
        // 是否开启服务器分片校验
        testChunks: true,
        // 服务器分片校验函数，秒传及断点续传基础
        checkChunkUploadedByResponse(chunk, message) {
          let skip = false;

          try {
            const objMessage = JSON.parse(message);
            if (objMessage.skipUpload) {
              skip = true;
            } else {
              skip = (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0;
            }
          } catch (e) {
            console.error(e);
          }

          return skip;
        },
        query: (file, chunk) => {
          return {
            ...file.params,
          };
        },
      };
      const fileStatusText = {
        success: '上传成功',
        error: '上传失败',
        uploading: '上传中',
        paused: '已暂停',
        waiting: '等待上传',
      };
      const customStatus = ref('');
      const panelShow = ref(false);
      const collapse = ref(false);
      const uploaderRef = ref();
      const uploadBtnRef = ref();
      let mergeFn = mergeFile;

      const uploader = computed(() => uploaderRef.value?.uploader);

      let customParams = {};
      watch(
        () => props.params,
        (data) => {
          if (data) {
            customParams = data;
          }
        }
      );
      // 自定义options
      function customizeOptions(opts) {
        // 自定义上传url
        if (opts.target) {
          uploader.value.opts.target = opts.target;
        }
        // 是否可以秒传、断点续传
        if (opts.testChunks !== undefined) {
          uploader.value.opts.testChunks = opts.testChunks;
        }
        // merge 的方法，类型为Function，返回Promise
        if (opts.mergeFn) {
          mergeFn = opts.mergeFn;
        }
        // 自定义文件上传类型
        // if (opts.accept) {
        //   const input = document.querySelector('#global-uploader-btn input');
        //   input.setAttribute(opts.accept, accept.join());
        // }
      }
      watch(
        () => props.options,
        (data) => {
          if (data) {
            setTimeout(() => {
              customizeOptions(data);
            }, 0);
          }
        }
      );
      function trigger(e) {
        Bus.emit(e);
        emit(e);
      }
      function statusSet(id, status) {
        const statusMap = {
          md5: {
            text: '校验MD5',
            bgc: '#fff',
          },
          merging: {
            text: '合并中',
            bgc: '#e2eeff',
          },
          transcoding: {
            text: '转码中',
            bgc: '#e2eeff',
          },
          failed: {
            text: '上传失败',
            bgc: '#e2eeff',
          },
        };

        customStatus.value = status;
        nextTick(() => {
          const statusTag = document.createElement('p');
          statusTag.className = `custom-status-${id} custom-status`;
          statusTag.innerText = statusMap[status].text;
          statusTag.style.backgroundColor = statusMap[status].bgc;

          const statusWrap = document.querySelector(
            `.file_${id} .uploader-file-status`
          );
          statusWrap.appendChild(statusTag);
        });
      }
      function statusRemove(id) {
        customStatus.value = '';
        nextTick(() => {
          const statusTag = document.querySelector(`.custom-status-${id}`);
          statusTag.remove();
        });
      }
      function error(msg) {
        Message.error(msg);
      }
      function computeMD5(file) {
        // 文件状态设为"计算MD5"
        statusSet(file.id, 'md5');
        // 暂停文件
        file.pause();
        // 计算MD5时隐藏”开始“按钮
        nextTick(() => {
          const div = document.querySelector(
            `.file-${file.id} .uploader-file-resume`
          );
          if (div && div.style) div.style.display = 'none';
        });

        // 开始计算MD5
        return new Promise((resolve, reject) => {
          generateMD5(file, {
            onProgress(currentChunk, chunks) {
              // 实时展示MD5的计算进度
              nextTick(() => {
                const md5ProgressText = `'校验MD5 ${(
                  (currentChunk / chunks) *
                  100
                ).toFixed(0)}%`;
                document.querySelector(`.custom-status-${file.id}`).innerText =
                  md5ProgressText;
              });
            },
            onSuccess(md5) {
              statusRemove(file.id);
              resolve(md5);
            },
            onError() {
              error(`文件${file.name}读取出错，请检查该文件`);
              file.cancel();
              statusRemove(file.id);
              reject();
            },
          });
        });
      }
      // md5计算完毕，开始上传
      function startUpload(file, md5) {
        file.uniqueIdentifier = md5;
        file.resume();
      }
      function onFileSuccess(rootFile, file, response, chunk) {
        const res = JSON.parse(response);

        // 服务端自定义的错误（即http状态码为200，但是是错误的情况），这种错误是Uploader无法拦截的
        if (!res.result) {
          error(res.message);
          // 文件状态设为“失败”
          statusSet(file.id, 'failed');
          return;
        }

        // 如果服务端返回了需要合并的参数
        if (res.needMerge) {
          // 文件状态设为“合并中”
          statusSet(file.id, 'merging');

          mergeFn({
            tempName: res.tempName,
            fileName: file.name,
            ...file.params,
          })
            .then((r) => {
              // 文件合并成功
              trigger('fileSuccess');

              statusRemove(file.id);
            })
            .catch((e) => {
              console.error(e);
            });

          // 不需要合并
        } else {
          trigger('fileSuccess');
          console.log('上传成功');
        }
      }
      function onFileProgress(rootFile, file, chunk) {
        console.log(
          `上传中 ${file.name}，chunk：${chunk.startByte / 1024 / 1024} ~ ${
            chunk.endByte / 1024 / 1024
          }`
        );
      }
      function onFileError(rootFile, file, response, chunk) {
        error(response);
      }
      function close() {
        uploader.value.cancel();
        panelShow.value = false;
      }
      /**
       * 新增的自定义的状态: 'md5'、'merging'、'transcoding'、'failed'
       * @param id
       * @param status
       */

      async function onFileAdded(file) {
        panelShow.value = true;
        trigger('fileAdded');
        // 将额外的参数赋值到每个文件上，以不同文件使用不同params的需求
        file.params = customParams.value;
        // 计算MD5
        const md5 = await computeMD5(file);
        startUpload(file, md5);
      }

      onMounted(() => {
        Bus.on('openUploader', ({ params = {}, options = {} }) => {
          customParams = params;

          customizeOptions(options);

          if (uploadBtnRef.value) {
            uploadBtnRef.value.$el.click();
          }
        });
      });

      return {
        initOptions,
        fileStatusText,
        customStatus,
        panelShow,
        collapse,
        uploaderRef,
        uploadBtnRef,
        onFileAdded,
        onFileSuccess,
        onFileProgress,
        onFileError,
        close,
      };
    },
  };
</script>

<style lang="less">
  #global-uploader {
    &:not(.global-uploader-single) {
      position: fixed;
      z-index: 20;
      right: 15px;
      bottom: 15px;
      box-sizing: border-box;
    }

    .uploader-app {
      width: 520px;
    }

    .file-panel {
      background-color: #fff;
      border: 1px solid #e2e2e2;
      border-radius: 7px 7px 0 0;
      box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);

      .file-title {
        display: flex;
        height: 40px;
        line-height: 40px;
        padding: 0 15px;
        border-bottom: 1px solid #ddd;

        .operate {
          flex: 1;
          text-align: right;

          .a-button {
            + .a-button {
              margin-left: 8px;
            }
          }
        }
      }

      .file-list {
        position: relative;
        height: 240px;
        overflow-x: hidden;
        overflow-y: auto;
        background-color: #fff;
        transition: all 0.3s;

        .file-item {
          background-color: #fff;
        }
      }

      &.collapse {
        .file-title {
          background-color: #e7ecf2;
        }
        .file-list {
          height: 0;
        }
      }
    }

    .no-file {
      position: absolute;
      top: 45%;
      left: 50%;
      transform: translate(-50%, -50%);
      color: #999;

      svg {
        vertical-align: text-bottom;
      }
    }

    .uploader-file {
      &.md5 {
        .uploader-file-resume {
          display: none;
        }
      }
    }

    .uploader-file-icon {
      &:before {
        content: '' !important;
      }

      &[icon='image'] {
        background: url(@/assets/images/image-icon.png);
      }
      &[icon='audio'] {
        background: url(@/assets/images/audio-icon.png);
        background-size: contain;
      }
      &[icon='video'] {
        background: url(@/assets/images/video-icon.png);
      }
      &[icon='document'] {
        background: url(@/assets/images/text-icon.png);
      }
      &[icon='unknown'] {
        background: url(@/assets/images/zip.png) no-repeat center;
        background-size: contain;
      }
    }

    .uploader-file-actions > span {
      margin-right: 6px;
    }

    .custom-status {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      z-index: 1;
    }
  }

  /* 隐藏上传按钮 */
  #global-uploader-btn {
    position: absolute;
    clip: rect(0, 0, 0, 0);
  }

  .global-uploader-single {
    #global-uploader-btn {
      position: relative;
    }
  }
</style>
